//
//  THIS CODE AND INFORMATION IS PROVIDED "AS IS" WITHOUT WARRANTY OF ANY
//  KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE
//  IMPLIED WARRANTIES OF MERCHANTABILITY AND/OR FITNESS FOR A PARTICULAR
//  PURPOSE. IT CAN BE DISTRIBUTED FREE OF CHARGE AS LONG AS THIS HEADER 
//  REMAINS UNCHANGED.
//
//  Email:  gustavo_franco@hotmail.com
//
//  Copyright (C) 2006 Franco, Gustavo 
//

using System;
using System.Collections.Generic;
using System.Drawing;

namespace D3.Pathing
{

    #region Structs

    public struct SpeedyAStarNode
    {
        public int F;
        public int G;
        public int H;
        public int PX;
        public int PY;
        public int X;
        public int Y;
    }

    #endregion

    #region Enum

    public enum NodeTypes
    {
        Start = 1,
        End = 2,
        Open = 4,
        Close = 8,
        Current = 16,
        Path = 32
    }

    public enum heurs
    {
        Manhattan = 1,
        MaxDXDY = 2,
        DiagonalShortCut = 3,
        Euclidean = 4,
        EuclideanNoSQR = 5,
        Custom1 = 6
    }

    #endregion

    #region Delegates

    public delegate void PathFinderDebugHandler(
        int fromX, int fromY, int x, int y, NodeTypes type, int totalCost, int cost);

    #endregion

    public class AStar : IPathFinder
    {
        #region Variables Declaration

        private readonly List<SpeedyAStarNode> mClose = new List<SpeedyAStarNode>();
        private readonly byte[,] mGrid;
        private readonly PriorityQueueB<SpeedyAStarNode> mOpen = new PriorityQueueB<SpeedyAStarNode>(new ComparePFNode());
        private bool mDiagonals = true;
        private heurs mFormula = heurs.Euclidean;
        private int mHEstimate = 2;
        private bool mHeavyDiagonals;
        private int mHoriz;
        private bool mPunishChangeDirection;
        private int mSearchLimit = 2000000;
        private bool mStop;
        private bool mStopped = true;
        private bool mTieBreaker;

        #endregion

        #region Constructors

        public AStar(byte[,] grid)
        {
            if (grid == null)
                throw new Exception("Grid cannot be null");

            mGrid = grid;
        }

        #endregion

        #region Properties

        public bool Stopped
        {
            get { return mStopped; }
        }

        public heurs Formula
        {
            get { return mFormula; }
            set { mFormula = value; }
        }

        public bool Diagonals
        {
            get { return mDiagonals; }
            set { mDiagonals = value; }
        }

        public bool HeavyDiagonals
        {
            get { return mHeavyDiagonals; }
            set { mHeavyDiagonals = value; }
        }

        public int HeuristicEstimate
        {
            get { return mHEstimate; }
            set { mHEstimate = value; }
        }

        public bool PunishChangeDirection
        {
            get { return mPunishChangeDirection; }
            set { mPunishChangeDirection = value; }
        }

        public bool TieBreaker
        {
            get { return mTieBreaker; }
            set { mTieBreaker = value; }
        }

        public int SearchLimit
        {
            get { return mSearchLimit; }
            set { mSearchLimit = value; }
        }

        public double CompletedTime { get; set; }

        public bool DebugProgress { get; set; }

        public bool DebugFoundPath { get; set; }

        #endregion

        #region Methods

        public void FindPathStop()
        {
            mStop = true;
        }

        public List<SpeedyAStarNode> FindPath(Point start, Point end)
        {
            mGrid[start.X, start.Y] = 4;
            SpeedyAStarNode parentNode;
            bool found = false;
            int gridX = mGrid.GetUpperBound(0);
            int gridY = mGrid.GetUpperBound(1);

            mStop = false;
            mStopped = false;
            mOpen.Clear();
            mClose.Clear();

            sbyte[,] direction;
            if (mDiagonals)
                direction = new sbyte[8,2] {{0, -1}, {1, 0}, {0, 1}, {-1, 0}, {1, -1}, {1, 1}, {-1, 1}, {-1, -1}};
            else
                direction = new sbyte[4,2] {{0, -1}, {1, 0}, {0, 1}, {-1, 0}};

            parentNode.G = 0;
            parentNode.H = mHEstimate;
            parentNode.F = parentNode.G + parentNode.H;
            parentNode.X = start.X;
            parentNode.Y = start.Y;
            parentNode.PX = parentNode.X;
            parentNode.PY = parentNode.Y;

            mOpen.Push(parentNode);


            while (mOpen.Count > 0 && !mStop)
            {
                parentNode = mOpen.Pop();


                if (parentNode.X == end.X && parentNode.Y == end.Y)
                {
                    mClose.Add(parentNode);
                    found = true;
                    break;
                }

                if (mClose.Count > mSearchLimit)
                {
                    mStopped = true;
                    return null;
                }

                if (mPunishChangeDirection)
                    mHoriz = (parentNode.X - parentNode.PX);

                //Lets calculate each successors
                for (int i = 0; i < (mDiagonals ? 8 : 4); i++)
                {
                    SpeedyAStarNode newNode;
                    newNode.X = parentNode.X + direction[i, 0];
                    newNode.Y = parentNode.Y + direction[i, 1];

                    if (newNode.X < 0 || newNode.Y < 0 || newNode.X >= gridX || newNode.Y >= gridY)
                        continue;

                    int newG;
                    if (mHeavyDiagonals && i > 3)
                        newG = parentNode.G + (int) (mGrid[newNode.X, newNode.Y]*2.41);
                    else
                        newG = parentNode.G + mGrid[newNode.X, newNode.Y];


                    if (newG == parentNode.G)
                    {
                        //Unbrekeable
                        continue;
                    }

                    if (mPunishChangeDirection)
                    {
                        if ((newNode.X - parentNode.X) != 0)
                        {
                            if (mHoriz == 0)
                                newG += 20;
                        }
                        if ((newNode.Y - parentNode.Y) != 0)
                        {
                            if (mHoriz != 0)
                                newG += 20;
                        }
                    }

                    int foundInOpenIndex = -1;
                    for (int j = 0; j < mOpen.Count; j++)
                    {
                        if (mOpen[j].X == newNode.X && mOpen[j].Y == newNode.Y)
                        {
                            foundInOpenIndex = j;
                            break;
                        }
                    }
                    if (foundInOpenIndex != -1 && mOpen[foundInOpenIndex].G <= newG)
                        continue;

                    int foundInCloseIndex = -1;
                    for (int j = 0; j < mClose.Count; j++)
                    {
                        if (mClose[j].X == newNode.X && mClose[j].Y == newNode.Y)
                        {
                            foundInCloseIndex = j;
                            break;
                        }
                    }
                    if (foundInCloseIndex != -1 && mClose[foundInCloseIndex].G <= newG)
                        continue;

                    newNode.PX = parentNode.X;
                    newNode.PY = parentNode.Y;
                    newNode.G = newG;

                    switch (mFormula)
                    {
                        default:
                        case heurs.Manhattan:
                            newNode.H = mHEstimate*(Math.Abs(newNode.X - end.X) + Math.Abs(newNode.Y - end.Y));
                            break;
                        case heurs.MaxDXDY:
                            newNode.H = mHEstimate*(Math.Max(Math.Abs(newNode.X - end.X), Math.Abs(newNode.Y - end.Y)));
                            break;
                        case heurs.DiagonalShortCut:
                            int h_diagonal = Math.Min(Math.Abs(newNode.X - end.X), Math.Abs(newNode.Y - end.Y));
                            int h_straight = (Math.Abs(newNode.X - end.X) + Math.Abs(newNode.Y - end.Y));
                            newNode.H = (mHEstimate*2)*h_diagonal + mHEstimate*(h_straight - 2*h_diagonal);
                            break;
                        case heurs.Euclidean:
                            newNode.H =
                                (int)
                                (mHEstimate*
                                 Math.Sqrt(Math.Pow((newNode.X - end.X), 2) + Math.Pow((newNode.Y - end.Y), 2)));
                            break;
                        case heurs.EuclideanNoSQR:
                            newNode.H =
                                (int) (mHEstimate*(Math.Pow((newNode.X - end.X), 2) + Math.Pow((newNode.Y - end.Y), 2)));
                            break;
                        case heurs.Custom1:
                            var dxy = new Point(Math.Abs(end.X - newNode.X), Math.Abs(end.Y - newNode.Y));
                            int Orthogonal = Math.Abs(dxy.X - dxy.Y);
                            int Diagonal = Math.Abs(((dxy.X + dxy.Y) - Orthogonal)/2);
                            newNode.H = mHEstimate*(Diagonal + Orthogonal + dxy.X + dxy.Y);
                            break;
                    }
                    if (mTieBreaker)
                    {
                        int dx1 = parentNode.X - end.X;
                        int dy1 = parentNode.Y - end.Y;
                        int dx2 = start.X - end.X;
                        int dy2 = start.Y - end.Y;
                        int cross = Math.Abs(dx1*dy2 - dx2*dy1);
                        newNode.H = (int) (newNode.H + cross*0.001);
                    }
                    newNode.F = newNode.G + newNode.H;

                    //It is faster if we leave the open node in the priority queue
                    //When it is removed, all nodes around will be closed, it will be ignored automatically
                    //if (foundInOpenIndex != -1)
                    //    mOpen.RemoveAt(foundInOpenIndex);

                    //if (foundInOpenIndex == -1)
                    mOpen.Push(newNode);
                }

                mClose.Add(parentNode);
            }

            if (found)
            {
                SpeedyAStarNode fNode = mClose[mClose.Count - 1];
                for (int i = mClose.Count - 1; i >= 0; i--)
                {
                    if (fNode.PX == mClose[i].X && fNode.PY == mClose[i].Y || i == mClose.Count - 1)
                    {
                        fNode = mClose[i];
                    }
                    else
                        mClose.RemoveAt(i);
                }
                mStopped = true;

                int i_c = 0;
                int skipCount = 6;
                var pfns = new List<SpeedyAStarNode>();
                foreach (SpeedyAStarNode pfn in mClose)
                {
                    if (i_c%skipCount == 0 || mClose.Count == i_c + 1) pfns.Add(pfn);
                    i_c++;
                }


                return pfns;
            }
            mStopped = true;
            return null;
        }

        #endregion

        #region Inner Classes

        internal class ComparePFNode : IComparer<SpeedyAStarNode>
        {
            #region IComparer<SpeedyAStarNode> Members

            public int Compare(SpeedyAStarNode x, SpeedyAStarNode y)
            {
                if (x.F > y.F)
                    return 1;
                else if (x.F < y.F)
                    return -1;
                return 0;
            }

            #endregion
        }

        #endregion

        /*[System.Runtime.InteropServices.DllImport("KERNEL32.DLL", EntryPoint="RtlZeroMemory")]
        public unsafe static extern bool ZeroMemory(byte* destination, int length);*/
    }
}